package it.trekker.dataexchange.service;

import java.io.IOException;
import java.util.Timer;
import java.util.TimerTask;
import it.trekker.converter.IJsonConverter;
import it.trekker.converter.JsonConverter;
import it.trekker.dataexchange.PoiRealTimeInfo;
import it.trekker.dataexchange.DataExchange;
import it.trekker.statistics.PoiDistanceStatistics;
import org.teleal.cling.android.AndroidUpnpService;
import org.teleal.cling.binding.LocalServiceBindingException;
import org.teleal.cling.binding.annotations.AnnotationLocalServiceBinder;
import org.teleal.cling.controlpoint.ActionCallback;
import org.teleal.cling.model.DefaultServiceManager;
import org.teleal.cling.model.ValidationException;
import org.teleal.cling.model.action.ActionArgumentValue;
import org.teleal.cling.model.action.ActionInvocation;
import org.teleal.cling.model.message.UpnpResponse;
import org.teleal.cling.model.meta.DeviceDetails;
import org.teleal.cling.model.meta.DeviceIdentity;
import org.teleal.cling.model.meta.LocalDevice;
import org.teleal.cling.model.meta.LocalService;
import org.teleal.cling.model.meta.ManufacturerDetails;
import org.teleal.cling.model.meta.ModelDetails;
import org.teleal.cling.model.meta.RemoteDevice;
import org.teleal.cling.model.meta.Service;
import org.teleal.cling.model.types.DeviceType;
import org.teleal.cling.model.types.InvalidValueException;
import org.teleal.cling.model.types.ServiceId;
import org.teleal.cling.model.types.UDADeviceType;
import org.teleal.cling.model.types.UDAServiceId;
import org.teleal.cling.model.types.UDN;
import org.teleal.cling.registry.DefaultRegistryListener;
import org.teleal.cling.registry.RegistrationException;
import org.teleal.cling.registry.Registry;
import org.teleal.cling.registry.RegistryListener;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.util.Log;

public class ClingDataExchangeService implements IDataExchangeService {

	private static final String TAG = ClingDataExchangeService.class.getSimpleName();

	private Context context;
	private PoiDistanceStatistics poiDistanceStatistics;
	private ServiceConnection upnpServiceConnection;
	private AndroidUpnpService upnpService;
	private RegistryListener registryListener;
	private Timer timerSearch;
	private boolean startServer;
	private boolean startClient;
	private static ClingDataExchangeService instance = null;

	private static final int DEFAULT_SEARCHING_INTERVAL = 2000;
	private int searchingInterval;


	/**
	 * Provide the ClingDataExchangeService instance if exist one, create it if doesn't exist.
	 * 
	 * @param Context: context
	 * @param PoiDistanceStatistics: a PoiDistanceStatistics instance
	 * 
	 * @return the existing ClingDataExchangeService instance or a new one.
	 */
	public static ClingDataExchangeService getInstanceAndCreateIfNotExist(Context context, PoiDistanceStatistics poiDistanceStatistics) {
		if(instance == null)
			instance = new ClingDataExchangeService(context, poiDistanceStatistics);
		
		return instance;
	}
	
	
	/**
	 * Provide the ClingDataExchangeService instance if exist.
	 * 
	 * @return the ClingDataExchangeService if exist, null otherwise.
	 */
	public static ClingDataExchangeService getInstance() {
		return instance;
	}
	
	
	private ClingDataExchangeService(Context context, PoiDistanceStatistics poiDistanceStatistics) {
		this.context = context;
		this.poiDistanceStatistics = poiDistanceStatistics;
		searchingInterval = DEFAULT_SEARCHING_INTERVAL;
	}




	//**************************************************************************//
	//********************************* Client *********************************//
	//**************************************************************************//

	public int getSearchingInterval() {
		return searchingInterval;
	}


	@Override
	public void setSearchingInterval(int searchingInterval) {
		this.searchingInterval = searchingInterval;
	}


	private void pullUpClient() {

		registryListener = createRegistryListener(upnpService);

		// Getting ready for future device advertisements
		upnpService.getRegistry().addListener(registryListener);

		// Search asynchronously for all devices
		// upnpService.getControlPoint().search();
		// startSearching();
	}


	private void tearDownClient() {
		stopSearching();
		
		if(startClient && upnpService != null) {
			upnpService.getRegistry().removeListener(registryListener);
		}
	}


	public void startSearching() {
		startSearching(this.searchingInterval);
	}


	public boolean startSearching(int searchingInterval) {
		if(!startClient)
			return false;
		
		timerSearch = new Timer();
		timerSearch.scheduleAtFixedRate(createTimerTaskSearch(), 0, searchingInterval);
		
		return true;
	}


	public void stopSearching() {
		if(startClient && timerSearch != null)
			timerSearch.cancel();
	}


	public boolean search() {
		if(!startClient)
			return false;
		
		upnpService.getControlPoint().search();
		
		return true;
	}


	private TimerTask createTimerTaskSearch() {
		return new TimerTask() {

			@Override
			public void run() {
				Log.i(TAG, "Searching...");
				upnpService.getControlPoint().search();
			}
		};

	}


	private ServiceConnection createServiceConnection() {
		return (new ServiceConnection() {

			@Override
			public void onServiceConnected(ComponentName className, IBinder service) {
				upnpService = (AndroidUpnpService) service;

				if(startClient) {
					Log.i(TAG, "Starting client");
					pullUpClient();
				}
				if(startServer) {
					Log.i(TAG, "Starting server");
					pullUpServer();
				}
			}

			@Override
			public void onServiceDisconnected(ComponentName className) {
				upnpService = null;
			}
		});
	}


	RegistryListener createRegistryListener(final AndroidUpnpService upnpService) {
		return (new DefaultRegistryListener() {

			ServiceId serviceId = new UDAServiceId("TrekkerDataExchange");

			@Override
			public void remoteDeviceAdded(Registry registry, RemoteDevice device) {

				Service dataExchange;
				if ((dataExchange = device.findService(serviceId)) != null) {

					Log.d(TAG, "Service discovered: " + dataExchange);
					executeAction(upnpService, dataExchange);

				}

			}

			@Override
			public void remoteDeviceRemoved(Registry registry, RemoteDevice device) {
				Service dataExchange;
				if ((dataExchange = device.findService(serviceId)) != null) {
					Log.d(TAG, "Service disappeared: " + dataExchange);
				}
			}

		});
	}


	protected void executeAction(AndroidUpnpService upnpService, Service dataExchangeService) {

		ActionInvocation getDataInvocation = new GetDataActionInvocation(dataExchangeService);

		// Executes asynchronous in the background
		upnpService.getControlPoint().execute(
				new ActionCallback(getDataInvocation) {

					@Override
					public void success(ActionInvocation invocation) {
						//						assert invocation.getOutput().length == 0;
						Log.d(TAG, "Successfully called action!");

						ActionArgumentValue output = invocation.getOutput("DataList");

						assert output != null;	// run this program with -ea (or -enableassertions argument)

						Log.d(TAG, "Data type: " + output.getDatatype());
						Log.d(TAG, "Value: " + output.getValue());

						String dataJson = (String) output.getValue();
						if(dataJson != null && !dataJson.equals("")) {
							IJsonConverter converter = new JsonConverter();
							PoiRealTimeInfo[] pois = converter.Json2PoiRealTimeInfoArray(dataJson);
							for(PoiRealTimeInfo poi : pois) {
								poiDistanceStatistics.addPoi(poi);
								Log.d(TAG, "Poi: " + poi);
							}
						}

					}

					@Override
					public void failure(ActionInvocation invocation,
							UpnpResponse operation,
							String defaultMsg) {
						Log.d(TAG, defaultMsg);
					}
				}
				);
	}


	protected class GetDataActionInvocation extends ActionInvocation {

		GetDataActionInvocation(Service service) {
			super(service.getAction("RetrieveData"));
			try {

				// Throws InvalidValueException if the value is of wrong type
				//				setInput("NewTargetValue", true);

			} catch (InvalidValueException ex) {
				Log.e(TAG, ex.getMessage());
			}
		}
	}





	//**************************************************************************//
	//********************************* Server *********************************//
	//**************************************************************************//

	private void pullUpServer() {
		try {

			upnpService.getRegistry().addDevice( createDevice() );

		} catch (RegistrationException e) {
			Log.e(TAG, "Error: ", e);
			e.printStackTrace();
		} catch (LocalServiceBindingException e) {
			Log.e(TAG, "Error: ", e);
			e.printStackTrace();
		} catch (ValidationException e) {
			Log.e(TAG, "Error: ", e);
			e.printStackTrace();
		} catch (IOException e) {
			Log.e(TAG, "Error: ", e);
			e.printStackTrace();
		}
	}


	private LocalDevice createDevice() throws ValidationException, LocalServiceBindingException, IOException {

		DeviceIdentity identity =
				new DeviceIdentity( UDN.uniqueSystemIdentifier("Data exchange") );

		DeviceType type =
				new UDADeviceType("DataExchange", 1);

		DeviceDetails details =
				new DeviceDetails(
						"Trekker Data Exchange device",
						new ManufacturerDetails("SP"),
						new ModelDetails(
								"TrekkerDataExchangeDevice",
								"The data exchange device",
								"v1"
							)
					);


		LocalService<DataExchange> dataExchangeService =
				new AnnotationLocalServiceBinder().read(DataExchange.class);

		dataExchangeService.setManager(
				new DefaultServiceManager(dataExchangeService, DataExchange.class)
				);

		//		return new LocalDevice(identity, type, details, icon, dataExchangeService);
		return new LocalDevice(identity, type, details, dataExchangeService);
	}





	//*************************************************************************//
	//******************************* Utilities *******************************//
	//*************************************************************************//

	/**
	 * Start UPnP server and client
	 */
	public void startUPnP() {
		startUPnP(true, true);
	}


	/**
	 * Start UPnP service
	 * 
	 * @param startServer: specify if server has to be started
	 * @param startClient: specify if client has to be started
	 */
	@Override
	public void startUPnP(boolean startServer, boolean startClient) {

		this.startServer = startServer;
		this.startClient = startClient;

		if(startServer || startClient) {
			upnpServiceConnection = createServiceConnection();

			context.bindService(
					new Intent(context, UPnPDataExchangeService.class),
					upnpServiceConnection,
					Context.BIND_AUTO_CREATE
				);
		}
	}


	@Override
	public void stopUPnP() {
		if(upnpService != null && upnpServiceConnection != null) {
			Log.i(TAG, "Stopping UPnP service");

			tearDownClient();

			context.unbindService(upnpServiceConnection);
		}

	}
}
